package org.infodavid.common.services;

import java.io.Serializable;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.persistence.PersistenceException;

import junit.framework.Assert;

import org.hamcrest.Description;
import org.jmock.Expectations;
import org.jmock.Mockery;
import org.jmock.api.Action;
import org.jmock.api.Invocation;
import org.jmock.lib.legacy.ClassImposteriser;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import org.infodavid.common.model.AbstractBeanTest;
import org.infodavid.common.model.AbstractPersistenceTest;
import org.infodavid.common.model.IUser;
import org.infodavid.common.model.criteria.IExtendedSearchCriteria;
import org.infodavid.common.persistence.IDataObject;
import org.infodavid.common.persistence.IDataService;
import org.infodavid.common.reflect.InstantiationHelper;
import org.infodavid.common.reflect.MethodUtil;

/**
 * The Class AbstractServiceTest.
 * @param <L> the generic type
 * @param <T> the generic type
 * @param <C> the generic type
 * @param <S> the generic type
 */
@SuppressWarnings({
    "unchecked", "rawtypes", "null", "boxing"
})
public abstract class AbstractGenericServiceTest<L extends IDataObject<Long>,T extends L,C extends IExtendedSearchCriteria,S extends IDataService>
    extends AbstractPersistenceTest {

	/**
	 * The Enum ECrudMethod.
	 */
	public enum ECrudMethod {

		/** The CREATE. */
		CREATE,
		/** The ADD. */
		ADD,
		/** The RETRIEVE. */
		RETRIEVE,
		/** The LIST. */
		LIST,
		/** The UPDATE. */
		UPDATE,
		/** The DELETE. */
		DELETE
	}

	/**
	 * The Class AddElementsAction.
	 * @param <T> the generic type
	 */
	@SuppressWarnings("hiding")
	public class AddElementsAction<T> implements Action {

		/** The elements. */
		private final List<T> elements;

		/** The parameter index. */
		private final int parameterIndex;

		/**
		 * Instantiates a new add elements action.
		 * @param arg0 the arg0
		 * @param arg1 the arg1
		 */
		public AddElementsAction(final int arg0, final List<T> arg1) {
			elements = arg1;
			parameterIndex = arg0;
		}

		/**
		 * See super class or interface. (non-Javadoc)
		 * @param description the description
		 * @see org.hamcrest.SelfDescribing#describeTo(org.hamcrest.Description)
		 */
		public void describeTo(final Description description) {
			description.appendText("adds ").appendValueList("", ", ", "", elements)
			    .appendText(" to a collection");
		}

		/**
		 * See super class or interface. (non-Javadoc)
		 * @param invocation the invocation
		 * @return the object
		 * @throws Throwable the throwable
		 * @see org.jmock.api.Invokable#invoke(org.jmock.api.Invocation)
		 */
		public Object invoke(final Invocation invocation) throws Throwable {
			final List<T> results = (List<T>)invocation.getParameter(parameterIndex);

			results.addAll(elements);

			return Long.valueOf(results.size());
		}
	}

	/**
	 * Adds the elements.
	 * @param <T> the generic type
	 * @param index the index
	 * @param newElements the new elements
	 * @return the action
	 */
	@SuppressWarnings("hiding")
	public <T> Action addElements(final int index, final T... newElements) {
		return new AddElementsAction<T>(index, Arrays.asList(newElements));
	}

	/** The application context. */
	protected IApplicationContext contextMock = null;

	/** The mockery. */
	private Mockery mockery = null;

	/** The service. */
	private Object service = null;

	/** The methods. */
	private Map<ECrudMethod,Method> methods = null;

	/** The user. */
	protected IUser userMock = null;

	/** Start date. */
	private long start = System.currentTimeMillis();

	/** The context properties. */
	private final Map<String,Serializable> properties = new HashMap<String,Serializable>();

	/**
	 * The Constructor.
	 * @param pService the service
	 * @param pMethods the methods
	 */
	public AbstractGenericServiceTest(final Object pService, final Map<ECrudMethod,String> pMethods) {
		service = pService;
		methods = new HashMap<ECrudMethod,Method>();

		final MethodUtil util = MethodUtil.getInstance();

		for (final Map.Entry<ECrudMethod,String> entry : pMethods.entrySet()) {
			methods.put(entry.getKey(), util.findMethod(pService.getClass(), entry.getValue()));
		}
	}

	/**
	 * Builds the criteria.
	 * @return the c
	 */
	public abstract C buildCriteria();

	/**
	 * Creates the.
	 * @param count the count
	 * @return the collection
	 * @throws Exception the exception
	 */
	public List<T> create(final int count) throws Exception {
		return getBeanTest().create(count);
	}

	/**
	 * Populate.
	 * @param count the count
	 * @return the list
	 * @throws Exception the exception
	 */
	public List<T> populate(final int count) throws Exception {
		final AbstractBeanTest<T> delegate = getBeanTest();

		return delegate.populate(count);
	}

	/**
	 * Gets the bean test.
	 * @return the bean test
	 */
	protected abstract AbstractBeanTest<T> getBeanTest();

	/**
	 * Expectations on persistence session.
	 */
	protected void expectationsOnPersistenceSession() {
		getMockery().checking(new Expectations() {
			{
				allowing(contextMock).getPersistenceSession();
				will(returnValue(getSession()));
			}
		});
	}

	/**
	 * Expectations on persistence session.
	 */
	protected void expectationsOnSecureService() {}

	/**
	 * Gets the context.
	 * @return the context
	 */
	public IApplicationContext getContextMock() {
		return contextMock;
	}

	/**
	 * Gets the data service class.
	 * @return the data service class
	 */
	public abstract Class<S> getDataServiceClass();

	/**
	 * Gets the mockery.
	 * @return the mockery
	 */
	public Mockery getMockery() {
		return mockery;
	}

	/**
	 * Gets the service.
	 * @return the service
	 */
	public Object getService() {
		return service;
	}

	/**
	 * Gets the user mock.
	 * @return the user mock
	 */
	public IUser getUserMock() {
		return userMock;
	}

	/**
	 * Returns the number of rows.
	 * @return the number of rows
	 * @throws PersistenceException the persistence exception
	 */
	protected long count() throws PersistenceException {
		final Object obj =
		    getSession().executeQuery(getBeanTest().getEntityClass(),
		        getBeanTest().getEntityName() + ".selectAll.count", null);

		return obj instanceof Number ? ((Number)obj).longValue() : 0;
	}

	/**
	 * Returns the properties.
	 * @return the properties
	 */
	protected Map<String,Serializable> getProperties() {
		return properties;
	}

	/**
	 * Sets the up.
	 * @throws Exception the exception
	 */
	@Override
	@Before
	public void setUp() throws Exception {
		super.setUp();

		mockery = new Mockery();

		mockery.setImposteriser(ClassImposteriser.INSTANCE);
		contextMock = mockery.mock(IApplicationContext.class);
		userMock = mockery.mock(IUser.class);

		getMockery().checking(new Expectations() {
			{
				allowing(contextMock).getProperties();
				will(returnValue(getProperties()));
			}
		});

		start = System.currentTimeMillis();
	}

	/**
	 * Tear down.
	 * @throws Exception the exception
	 */
	@Override
	@After
	public void tearDown() throws Exception {
		if (getMockery() != null) {
			getMockery().assertIsSatisfied();
		}

		InstantiationHelper.getInstance().unregister(getDataServiceClass());

		super.tearDown();
	}

	/**
	 * Invoke.
	 * @param method the method
	 * @param args the args
	 * @return the object
	 * @throws Throwable the throwable
	 */
	protected Object invoke(final ECrudMethod method, final Object... args) throws Throwable {
		final Method meth = methods.get(method);
		Object result;

		if (meth == null) {
			throw new NoSuchMethodException(method.toString());
		}

		try {
			result = meth.invoke(getService(), args);
		}
		catch (final InvocationTargetException e) {
			throw e.getCause();
		}

		return result;
	}

	/**
	 * Test add.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testAdd() throws Throwable {
		final T value = create(1).get(0);
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.ADD, getContextMock(), value);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNull("An exception occurs", exception);
		Assert.assertNotNull("Data is null", value);
		Assert.assertNotNull("Creation date is not valid", value.getCreationDate());
		Assert.assertTrue("Creation date is not valid", value.getCreationDate().getMillis() >= start
		    && value.getCreationDate().getMillis() <= System.currentTimeMillis());
		Assert.assertNotNull("Modification date is not valid", value.getModificationDate());
		Assert.assertTrue("Modification date is not valid",
		    value.getModificationDate().getMillis() >= start
		        && value.getModificationDate().getMillis() <= System.currentTimeMillis());
		Assert.assertNotNull("Key is null", value.getKey());
	}

	/**
	 * Test add.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testAddUsingAnInvalidContext() throws Throwable {
		final T value = create(1).get(0);
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.ADD, null, value);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNotNull("An exception occurs", exception);
		Assert.assertEquals("Wrong exception", IllegalArgumentException.class, exception.getClass());
	}

	/**
	 * Test add.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testAddUsingNullData() throws Throwable {
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.ADD, getContextMock(), null);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNotNull("An exception occurs", exception);
		Assert.assertEquals("Wrong exception", IllegalArgumentException.class, exception.getClass());
	}

	/**
	 * Test create.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testCreate() throws Throwable {
		final Object instance = invoke(ECrudMethod.CREATE);

		Assert.assertNotNull("Object not built", instance);
		Assert
		    .assertTrue("Wrong type of instance", getBeanTest().getEntityClass().isInstance(instance));
	}

	/**
	 * Test get.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testGet() throws Throwable {
		final T value = populate(ITEMS_TO_INSERT).get(0);
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.RETRIEVE, getContextMock(), value.getKey());
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNull("An exception occurs", exception);
		Assert.assertNotNull("Data is null", value);
		Assert.assertNotNull("Key is null", value.getKey());
	}

	/**
	 * Test get entities.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testGetEntities() throws Throwable {
		final T value = populate(ITEMS_TO_INSERT).get(0);
		final List<L> results = new ArrayList<L>();
		final C criteria = buildCriteria();
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.LIST, getContextMock(), results, criteria);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNull("An exception occurs", exception);
		Assert.assertNotNull("Data is null", value);
		Assert.assertNotNull("Key is null", value.getKey());
	}

	/**
	 * Test get entities.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testGetEntitiesUsingAnInvalidContext() throws Throwable {
		populate(ITEMS_TO_INSERT).get(0);
		final List<L> results = new ArrayList<L>();
		final C criteria = buildCriteria();
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.LIST, null, results, criteria);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNotNull("An exception occurs", exception);
		Assert.assertEquals("Wrong exception", IllegalArgumentException.class, exception.getClass());
	}

	/**
	 * Test get entities.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testGetEntitiesUsingAnInvalidCriteria() throws Throwable {
		final T value = populate(ITEMS_TO_INSERT).get(0);
		final List<L> results = new ArrayList<L>();
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.LIST, getContextMock(), results, null);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNull("An exception occurs", exception);
		Assert.assertNotNull("Data is null", value);
		Assert.assertNotNull("Key is null", value.getKey());
	}

	/**
	 * Test get entities.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testGetEntitiesUsingAnInvalidList() throws Throwable {
		populate(ITEMS_TO_INSERT).get(0);
		final C criteria = buildCriteria();
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.LIST, getContextMock(), null, criteria);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNotNull("An exception occurs", exception);
		Assert.assertEquals("Wrong exception", IllegalArgumentException.class, exception.getClass());
	}

	/**
	 * Test get.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testGetUsingAnInvalidContext() throws Throwable {
		final T value = populate(ITEMS_TO_INSERT).get(0);
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.RETRIEVE, null, value.getKey());
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNotNull("An exception occurs", exception);
		Assert.assertEquals("Wrong exception", IllegalArgumentException.class, exception.getClass());
	}

	/**
	 * Test get.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testGetUsingAnInvalidKey() throws Throwable {
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.RETRIEVE, getContextMock(), -1);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNull("An exception occurs", exception);
	}

	/**
	 * Test remove.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testRemove() throws Throwable {
		final T value = populate(ITEMS_TO_INSERT).get(ITEMS_TO_INSERT - 1);
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.DELETE, getContextMock(), value.getKey());
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNull("An exception occurs", exception);
		Assert.assertNotNull("Data is null", value);
	}

	/**
	 * Test remove.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testRemoveUsingAnInvalidKey() throws Throwable {
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.DELETE, getContextMock(), -1);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNotNull("An exception occurs", exception);
		Assert.assertEquals("Wrong exception", IllegalArgumentException.class, exception.getClass());
	}

	/**
	 * Test update.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testUpdate() throws Throwable {
		final T value = populate(ITEMS_TO_INSERT).get(0);
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.UPDATE, getContextMock(), value);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNull("An exception occurs", exception);
		Assert.assertNotNull("Data is null", value);
		Assert.assertNotNull("Modification date is not valid", value.getModificationDate());
		Assert.assertTrue("Modification date is not valid",
		    value.getModificationDate().getMillis() >= start
		        && value.getModificationDate().getMillis() <= System.currentTimeMillis());
		Assert.assertNotNull("Key is null", value.getKey());
	}

	/**
	 * Test update.
	 * @throws Throwable the throwable
	 */
	@Test
	public void testUpdateUsingNullData() throws Throwable {
		Exception exception = null;

		expectationsOnPersistenceSession();
		expectationsOnSecureService();

		try {
			invoke(ECrudMethod.UPDATE, getContextMock(), null);
		}
		catch (final Exception e) {
			e.printStackTrace();
			exception = e;
		}

		Assert.assertNotNull("An exception occurs", exception);
		Assert.assertEquals("Wrong exception", IllegalArgumentException.class, exception.getClass());
	}
}
